require 'java'

module JRake
  @@logger = nil
  @@servlets = {}

  ServletInfo = Struct.new(:servlet_path, :servlet_class_name, :class_path, :build_target)

#  module_function :register_servlet
  
  class << self
    
    def register_servlet(servlet_name, servlet_class_name, class_path, build_target)
      @@servlets[servlet_name] = ServletInfo.new(servlet_name, servlet_class_name, class_path, build_target)
    end

    # Logging methods
        
    def with_logger(logger)
      @@logger = logger
      yield
    ensure
      @@logger = nil
    end
    
    def log(msg)
      msg = "[#{java.lang.Thread::currentThread.name}] #{msg}\n" 
      if @@logger
        @@logger.write(msg)
        @@logger.flush
      end
      print msg
    end

    # Servlet methods
    
    def lookup_servlet(url_path)
      @@servlets.each do |servlet_path,servlet_info|
        if url_path == servlet_path
          return servlet_info
        elsif url_path =~ /#{servlet_path}\/.*/
          return servlet_info
        end
      end
      return nil
    end    
    
    # Delete
    
    def recursive_delete(dir)
      return unless File.exist?(dir)
      list = []
      collect_files(list, dir)
      JRake::log "deleting #{list.size} file(s)..."
      list.each do |file|
        if File.directory?(file)
          Dir.rmdir(file)
        else
          if File.delete(file) != 1
            JRake::log "FAILED\n"
            JRake::fail
          end
        end
      end
      JRake::log "done\n"
    end

    def collect_files(list, file)
      if File.directory?(file) 
        Dir.foreach(file) do |child| 
          next if child == '.' || child == '..'
          path = file + '/' + child
          collect_files(list, path)
        end
        list << file
      else
        list << file
      end
    end
    
    # Javac 
    
    def javac(src_dir, dest_dir, classpath)
      Dir::mkdir(dest_dir) unless File::exist?(dest_dir)

      java_files = get_out_of_date_files(src_dir, dest_dir)

      unless java_files.empty?
        JRake::log "compiling #{java_files.size} java file(s)..."

        args = ['-Xlint', '-sourcepath', '', '-classpath', classpath.join(java.io.File.pathSeparator), '-d', dest_dir, *java_files]

        buf = java.io.StringWriter.new    

        include_class('com.sun.tools.javac.Main')
        if Main.compile(to_java_array(java.lang.String, args), java.io.PrintWriter.new(buf)) != 0
          JRake::log "FAILED\n" + buf.to_s
          JRake::fail
        end
        JRake::log "done\n"
      end
    end

    def get_out_of_date_files(src_dir, dest_dir)
      FileList["#{src_dir}/**/*.java"].reject do |java_file|
        class_file = dest_dir + java_file[src_dir.length, java_file.length - src_dir.length - '.java'.length] + '.class'

        # todo: figure out why File.ctime doesn't work
        File.exist?(class_file) && java.io.File.new(class_file).lastModified > java.io.File.new(java_file).lastModified
      end
    end    
    
    # JUnit
    
    def junit(test_class_dir, class_path)
      # Append the class_dir to the class_path, if necessary
      class_path += [test_class_dir] unless class_path.member?(test_class_dir)

      # Make sure class_dir has trailing slash
      unless test_class_dir[-1,1] == '/'
        test_class_dir = test_class_dir + '/'
      end

      # Scan class_dir for test class files
      class_names = []
      FileList["#{test_class_dir}/**/*Test.class"].each do |class_file|
        class_names << class_file[test_class_dir.length, class_file.length - test_class_dir.length - '.class'.length].gsub('/', '.')
      end
      if class_names.empty?
        JRake::log "No test classes found" 
        JRake::fail 
      end
      
      # Load the test classes via a new classloader
      classes = load_classes(class_path, class_names)

      # Run the tests
      include_class('org.junit.runner.JUnitCore')
      runner = JUnitCore.new
    #  include_class('org.junit.internal.runners.TextListener')
    #  runner.add_listener(TextListener.new)
      JRake::log "running tests..."
      result = runner.run(to_java_array(java.lang.Class, classes))
      if result.was_successful
        JRake::log "done"
      else
        JRake::log "FAILED"
      end
      JRake::log "Time: #{result.run_time/1000.0}"
      count = 1
      result.failures.each do |failure|
        JRake::log(count.to_s + ') ' + failure.description.display_name.to_s + "\n")
        i = 0
        while i < failure.exception.stack_trace.length do 
          element = failure.exception.stack_trace[i]
          if element.class_name == 'org.junit.internal.runners.TestMethodRunner'
            break
          end
          JRake::log "        " + element.to_s + "\n"
          i += 1
        end
        count += 1
      end
      if result.was_successful
        JRake.log("OK (#{result.run_count} tests)")
      else
        JRake.log("FAILURES!!!")
        JRake.log("Tests run: #{result.run_count}, Failures: #{result.failure_count}\n")
        JRake::fail("Unit tests failed")
      end
    end
    
    # Class loading

    def load_classes(class_path, class_names)

      # Make sure directories have a trailing slash, otherwise URLClassLoader ignores them
      class_path.each { |element| element << '/' if FileTest.directory?(element) && element[-1,1] != '/' }

      # Convert classpath elements to URLs
      urls = to_java_array(java.net.URL, class_path.map { |element| java.net.URL.new('file:' + element) })

      # Create a class loader for the specified class path
      loader = java.net.URLClassLoader.new(urls)  
      return class_names.map { |class_name| loader.loadClass(class_name) }

    end

    def load_class(class_path, class_name) 
      load_classes(class_path, [class_name])[0]
    end

    def to_java_array(element_type, ruby_array)
      java_array = java.lang.reflect.Array.newInstance(element_type, ruby_array.size)
      ruby_array.each_index { |i| java_array[i] = ruby_array[i] }
      return java_array
    end
  end
  
  def JRake.fail
    raise FailureException.new
  end
  
  class FailureException < Exception
  end
  
end

